package com.nulldev.util.net;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.Proxy;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.ServerSocket;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Enumeration;
import java.util.concurrent.ThreadLocalRandom;
import java.util.regex.Pattern;

import com.nulldev.util.java.JVMUtil;
import com.nulldev.util.math.MathUtil;
import com.nulldev.util.os.OSUtil;
import com.nulldev.util.other.ArrayUtils;
import com.nulldev.util.other.BooleanUtil;
import com.nulldev.util.uri.URIUtil;

public class IPUtil {
	/**
	 * Linux Only <br>
	 * virbr is used by libvirt for virtual machine network bridges
	 */
	public static final String LIBVIRT_VIRTUAL_BRIDGE = "virbr";

	private static boolean enableLibVirtPatch = OSUtil.PackageUtil.isPackage("libvirtd")
			&& !JVMUtil.isArg("--IPUtil.disableLibVirtFix");
	public static boolean enableLibVirtPatch() {
		return enableLibVirtPatch;
	}
	
	public static void enableLibVirtPatch(final boolean value) {
		enableLibVirtPatch = value;
	}
	
	/**
 	* Returns an <code>InetAddress</code> object encapsulating what is most likely the machine's LAN IP address.
 	* <p/>
 	* This method is intended for use as a replacement of JDK method <code>InetAddress.getLocalHost</code>, because
 	* that method is ambiguous on Linux systems. Linux systems enumerate the loopback network interface the same
 	* way as regular LAN network interfaces, but the JDK <code>InetAddress.getLocalHost</code> method does not
 	* specify the algorithm used to select the address returned under such circumstances, and will often return the
 	* loopback address, which is not valid for network communication. Details
 	* <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4665037">here</a>.
 	* <p/>
 	* This method will scan all IP addresses on all network interfaces on the host machine to determine the IP address
 	* most likely to be the machine's LAN address. If the machine has multiple IP addresses, this method will prefer
 	* a site-local IP address (e.g. 192.168.x.x or 10.10.x.x, usually IPv4) if the machine has one (and will return the
 	* first site-local address if the machine has more than one), but if the machine does not hold a site-local
 	* address, this method will return simply the first non-loopback address found (IPv4 or IPv6).
 	* <p/>
 	* If this method cannot find a non-loopback address using this selection algorithm, it will fall back to
 	* calling and returning the result of JDK method <code>InetAddress.getLocalHost</code>.
 	* <p/>
 	*
 	* @throws UnknownHostException If the LAN address of the machine cannot be found.
 	*/
	public static InetAddress getLocalHostLANAddress() throws UnknownHostException {
    	try {
        	InetAddress candidateAddress = null;
        	// Iterate all NICs (network interface cards)...
        	for (Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); ifaces.hasMoreElements();) {
            	NetworkInterface iface = (NetworkInterface) ifaces.nextElement();
            	if (enableLibVirtPatch) {
            		if (OSUtil.isLinux() && (iface.getName().startsWith(LIBVIRT_VIRTUAL_BRIDGE)
            				|| (iface.getName().startsWith("virbr")) || (iface.getName().startsWith("pan")))) {
                		continue;
                	}
            	}
            	//JVMUtil.async_println(iface.getDisplayName() + ",name=" + iface.getName() + ",ip=" + iface.getInterfaceAddresses().get(0).getAddress());
            	// Iterate all IP addresses assigned to each card...
            	for (Enumeration inetAddrs = iface.getInetAddresses(); inetAddrs.hasMoreElements();) {
                	InetAddress inetAddr = (InetAddress) inetAddrs.nextElement();
                	if (!inetAddr.isLoopbackAddress()) {
                    	if (inetAddr.isSiteLocalAddress()) {
                        	// Found non-loopback site-local address. Return it immediately...
                        	return inetAddr;
                    	}
                    	else if (candidateAddress == null) {
                        	// Found non-loopback address, but not necessarily site-local.
                        	// Store it as a candidate to be returned if site-local address is not subsequently found...
                        	candidateAddress = inetAddr;
                        	// Note that we don't repeatedly assign non-loopback non-site-local addresses as candidates,
                        	// only the first. For subsequent iterations, candidate will be non-null.
                    	}
                	}
            	}
        	}
        	if (candidateAddress != null) {
            	// We did not find a site-local address, but we found some other non-loopback address.
            	// Server might have a non-site-local address assigned to its NIC (or it might be running
            	// IPv6 which deprecates the "site-local" concept).
            	// Return this non-loopback candidate address...
            	return candidateAddress;
        	}
        	// At this point, we did not find a non-loopback address.
        	// Fall back to returning whatever InetAddress.getLocalHost() returns...
        	InetAddress jdkSuppliedAddress = InetAddress.getLocalHost();
        	if (jdkSuppliedAddress == null) {
            	throw new UnknownHostException("The JDK InetAddress.getLocalHost() method unexpectedly returned null.");
        	}
        	return jdkSuppliedAddress;
    	}
    	catch (Exception e) {
        	UnknownHostException unknownHostException = new UnknownHostException("Failed to determine LAN address: " + e);
        	unknownHostException.initCause(e);
        	throw unknownHostException;
    	}
	}
	
	public static String getHostName(final String ip) throws Exception {
		final InetAddress adr = InetAddress.getByName(ip);
		return adr.getCanonicalHostName();
	}

	public static String getHostAddress(final String ip) throws Exception {
		final InetAddress adr = InetAddress.getByName(ip);
		return adr.getHostAddress();
	}
	
	public static String getInternetLocalIPS() throws UnknownHostException {
		final InetAddress local = InetAddress.getLocalHost();
		return local.getHostAddress();
	}
	
	public static String getInternetLocalIP_Safe() {
		try {
			final InetAddress local = InetAddress.getLocalHost();
			return local.getHostAddress();
		}catch (Exception e) {
			return "127.0.0.1";
		}
	}
	
	public static InetAddress getInternetLocalIP_SafeInet() {
		try {
			final InetAddress local = InetAddress.getLocalHost();
			return local;
		}catch (Exception e) {
			try {
				return InetAddress.getByName("127.0.0.1");
			}catch (Exception e1) {
				return null;
			}
		}
	}
	
	public static InetAddress getInternetLocalIP() throws UnknownHostException {
		final InetAddress local = InetAddress.getLocalHost();
		final String lx = local.getHostAddress();
		if (!lx.startsWith("127.") && !lx.startsWith("0.")) {
			return InetAddress.getByName("127.0.0.1");
		}
		return InetAddress.getByName(lx);
	}
	
	public static int nextFreePort(int from, int to) {
	    int port = MathUtil.randInt(from, to);
	    while (true) {
	        if (isLocalPortFree(port)) {
	            return prevPort = port;
	        } else {
	            port = MathUtil.randInt(from, to);
	        }
	    }
	}
	
	public static int nextFreePortIterated(int from, int to) {
//	    while (true) {
//	        if (isLocalPortFree(port)) {
//	            return prevPort = port;
//	        } else {
//	            port = MathUtil.randInt(from, to);
//	        }
//	    }
		for (int i = from; i < to; i++) {
	        if (isLocalPortFree(i)) {
	            return prevPort = i;
	        }
		}
		return nextFreePort(from, to);
	}

	private static int prevPort;
	public static boolean isLocalPortFree(int port) {
	    try {
	        new ServerSocket(port).close();
	        return true;
	    } catch (IOException e) {
	        return false;
	    }
	}
	
	public static boolean isPortFree(int port) {
	    try {
	        new ServerSocket(port, 0, getLocalHostLANAddress()).close();
	        return true;
	    } catch (IOException e) {
	        return false;
	    }
	}

	public static int nextFreePort() {
		return prevPort = nextFreePort(10, 65530);
	}
	
	public static int prevGeneratedPort() {
		return prevPort;
	}
	
	public static int clampPort(final int value) {
		return MathUtil.clamp(1, 65535, value);
	}

	public static String getPublicIP() {
		try {
			final URL whatismyip = new URL("http://checkip.amazonaws.com");
			final InputStream is = whatismyip.openStream();
			final InputStreamReader isr = new InputStreamReader(is);
			final BufferedReader br = new BufferedReader(isr);
			final String ip = br.readLine();
			br.close();
			isr.close();
			is.close();
			return ip;
		}catch (Exception e) {
			return "127.0.0.1";
		}
	}
	
	public static InetAddress getPublicIPAsInet() {
		try {
			final URL whatismyip = new URL("http://checkip.amazonaws.com");
			final InputStream is = whatismyip.openStream();
			final InputStreamReader isr = new InputStreamReader(is);
			final BufferedReader br = new BufferedReader(isr);
			final String ip = br.readLine();
			br.close();
			isr.close();
			is.close();
			return InetAddress.getByName(ip);
		}catch (Exception e) {
			try {
				return getLocalHostLANAddress();
			} catch (UnknownHostException e1) {
				try {
					return getInternetLocalIP();
				} catch (UnknownHostException e2) {
					try {
						return InetAddress.getLocalHost();
					} catch (UnknownHostException e3) {
						return null;
					}
				}
			}
		}
	}
	
	public static String getPublicIP(final Proxy p) {
		try {
			final URL whatismyip = new URL("http://checkip.amazonaws.com");
			final HttpURLConnection _is = (HttpURLConnection) whatismyip.openConnection(p);
			final InputStream is = _is.getInputStream();
			final InputStreamReader isr = new InputStreamReader(is);
			final BufferedReader br = new BufferedReader(isr);
			final String ip = br.readLine();
			br.close();
			isr.close();
			is.close();
			return ip;
		}catch (Exception e) {
			return "127.0.0.1";
		}
	}

	public static String getLocalHostLANAddress_safe() {
		try {
			return getLocalHostLANAddress().getHostAddress();
		}catch (Exception e) {
			return getInternetLocalIP_Safe();
		}
	}
	
	public static InetAddress getLocalHostLANAddress_safeinet() {
		try {
			return getLocalHostLANAddress();
		}catch (Exception e) {
			return getInternetLocalIP_SafeInet();
		}
	}
	
	public static String generateRandomIP() {
		return MathUtil.randInt(0, 255) + "." + MathUtil.randInt(0, 255) + "." + MathUtil.randInt(0, 255) + "." + MathUtil.randInt(0, 255);
	}
	
	public static long ipToLong(final InetAddress ip) {
        byte[] octets = ip.getAddress();
        long result = 0;
        for (byte octet : octets) {
            result <<= 8;
            result |= octet & 0xff;
        }
        return result;
    }
	
	public static boolean inRange(final InetAddress address, final InetAddress min, final InetAddress max) {
		final long a = ipToLong(address);
		final long mi = ipToLong(min);
		final long ma = ipToLong(max);
		return MathUtil.between(a, mi, ma);
	}

	public static short nextFreePortS() {
		return (short) MathUtil.randInt(0, Short.MAX_VALUE);
	}

	public static boolean online() {
		try {
			URIUtil.downloadPage(URIUtil.safeCreate("http://www.google.com"));
			return true;
		}catch (Throwable e) {
			return false;
		}
	}
	
	public static boolean online(final Proxy proxy) {
		try {
			URIUtil.downloadPage(URIUtil.safeCreate("http://www.google.com"), true, proxy);
			return true;
		}catch (Throwable e) {
			return false;
		}
	}
	
	public static void preferIPv6(final boolean ipv6) {
		System.setProperty("java.net.preferIPv6Addresses", String.valueOf(ipv6));
	}
	
	public static boolean preferIPv6() {
		return BooleanUtil.safeParse(System.getProperty("java.net.preferIPv6Addresses"));
	}
	
	private static final Pattern IPV4_PATTERN =
	        Pattern.compile(
	                "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$");

	private static final Pattern IPV6_STD_PATTERN =
	        Pattern.compile(
	                "^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$");

	private static final Pattern IPV6_HEX_COMPRESSED_PATTERN =
	        Pattern.compile(
	                "^((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)$");

	public static boolean isIPv4Address(final String input) {
		return IPV4_PATTERN.matcher(input).matches();
	}

	public static boolean isIPv6StdAddress(final String input) {
		return IPV6_STD_PATTERN.matcher(input).matches();
	}

	public static boolean isIPv6HexCompressedAddress(final String input) {
	    return IPV6_HEX_COMPRESSED_PATTERN.matcher(input).matches();
	}

	public static boolean isIPv6Address(final String input) {
	    return isIPv6StdAddress(input) || isIPv6HexCompressedAddress(input);
	}

	public static boolean isIP(final String ip) {
		return isIPv4Address(ip) || isIPv6Address(ip);
	}
}
